iT邦幫忙

2023 iThome 鐵人賽

DAY 18
1
Modern Web

深入淺出,完整認識 Next.js 13 !系列 第 18

Day 18 - Next.js 13 App Router 跨路由共用 UI:Layout 與 Template

  • 分享至 

  • xImage
  •  

昨天最後有提到,假設我們希望某個 route segment 的 children segments 之間,可以共用一個 component,比方說 /dashboard/settings 和 /dashboard/profile 都有一個相同的 header,我們可以使用 App Router 中的兩個特殊檔案 - layout.tsxtemplate.tsx

究竟兩者要如何使用?兩者間又有什麼差異呢?讓我們來一探究竟吧!

特殊檔案的副檔名可以接受 .js, .jsx, .tsx


Layouts

假如希望路由切換時,共用的 components 不會 re-render,達到 persistent layout,可以使用 layout.tsx 來建制 Layouts。

我們在 Day 09 已經簡單介紹過 layout 的用法,還沒讀過文章的讀者可以參考 Day 09 文章

簡單來說,你在 layout.tsx 中 default export 的 component,可以加入你想共用的 components 和一個 children props,這樣該 route segment 的所有 children segments 都會被傳進 children props 中,舉例來說:

假如我想讓 /dashboard 底下的所有 route segments ( ex: /dashboard/settings , /dashboard/profile, /dashboard/notifications ) 都有一個共用的 <Header>,我可以在 /dashboard 中建立 layout.tsx,並將 Header import 進 DashboardLayout 中:

/* /dashboard/layout.tsx */
import React from 'react';
import Header from './Header';


export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <>
      <Header />
      {children}
    </>
  );

這樣 /dashboard 的所有子路就都會有 <Header>
layout demo

這邊有個重點,Layout 中不需要 re-render 的 components,在切換路由時不會 re-render,因此會保留 state 和互動的狀態 ( ex: 滾輪位置 )。這也是 layout 和 template 最主要的差異,等介紹完 template 後我們再來做個實驗比較兩者差異。

在使用 layout 時有幾點需要注意:

  1. Layouts 無法傳 props 給 children components。 假如要共享 state,需要靠 useContext hook 或 Redux、Zustand 等 state 管理工具。
  2. Layouts 也可以 fetch data,但假如 layout 和 children components 要用到同一支 API 的資料,因為沒辦法傳 props,所以必須在 layout 和 children components 都 fetch data。

Next 預設會將 fetch result 存到快取,所以不用擔心重複 fetch 相同 api endpoint 造成效能負擔,細節可參考 Day 28 文章

  1. page.tsxlayout.tsx 可放在同一層資料夾中,page.tsx 也會套用 layout。所以上述例子,/dashboard 的 UI 也會有 Header。

Templates

Templates 和 Layouts 的概念類似,差別在路由切換時,templates 中所有 components 都會 re-render,因此不會記住當前 state 和互動狀態。

做個小實驗,我們在 Header 中加入兩個功能:

  1. 點擊 Header 中的選項時,會拜訪對應的 route
  2. 當前 route 對應的選項會呈現藍底白字

根據上述需求,我寫了一段簡單的程式碼:

<Link> 是用來做路由跳轉;usePathname是用來取得 URL path,後面文章會提到。

/* app/dashboard/Header.tsx */
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';

const options = ['Settings', 'Profile', 'Notifications', 'Blog', 'Support'];

export default function Header() {
  // 選項會對應到最後一個 route segment
  const currentPath = usePathname();
  const selectedOption = currentPath.split('/').pop();

  return (
    <div className='...'>
      <div className='...'>
        {options.map((option) => (
          <Link key={option} href={`/dashboard/${option.toLowerCase()}`}>
            <button
              key={option}
              className={`... ${
                option.toLowerCase() === selectedOption &&
                'bg-blue-500 text-white'
              }`}
            >
              {option}
            </button>
          </Link>
        ))}
      </div>
    </div>
  );
}

假如使用 layout.tsx,當我們滑到 Header 最右邊,點擊 support 時,頁面會切換到 /dashboard/support,而且 Header 滾輪依然在最右邊:
layout demo

但假如我們改成 template.tsx,裡面內容維持一樣。當滑到 Header 最右邊點擊 support,路由切換後,Header 的滾輪會回到初始位置:
template demo

所以 layout 和 template 都能用來讓子路由共享 components,但兩者的差異在於,當路由切換時,layout 不會 re-render,而 template 會 re-render。

useSelectedLayoutSegment

上面 sample code 中,我們是用 usePathname 取得 URL path,再從 URL path 取出最後一個 route segment 來判斷目前的選項。今天的最後,想跟大家分享另一個方法 - useSelectedLayoutSegment

useSelectedLayoutSegment 是一個用來判斷 layout 底下一層 的 route segments 中,active route segment 是哪個 segment 的 hook。我們一樣來看範例,可能會比較好理解:

上述 <Header> 的 sample code 中,我們是用網址的最後一個 route segment 來判斷哪個選項該是藍底白字。因為我們要的 route segment 剛好在 layout 底下一層,我們也可以用 useSelectedLayoutSegment 來取得 active route segment。比方說 /dashboard/settings 的 active route segment 就會是 settings;/dashbord/profile 的 active route segment 就會是 profile,以此類推。

所以我們也可以讓和 active segment 相同的選項改為藍底白字:

/* app/dashboard/Header.tsx */
'use client';
import Link from 'next/link';
import { useSelectedLayoutSegment } from 'next/navigation';

const options = ['Settings', 'Profile', 'Notifications', 'Blog', 'Support'];

export default function Header() {
  // 取得當前的 active segment
  const activeSegment = useSelectedLayoutSegment();

  return (
    <div className='...'>
      <div className='...'>
        {options.map((option) => (
          <Link key={option} href={`/dashboard/${option.toLowerCase()}`}>
            <button
              key={option}
              className={`... ${
                option.toLowerCase() === activeSegment &&
                'bg-blue-500 text-white'
              }`}
            >
              {option}
            </button>
          </Link>
        ))}
      </div>
   

以上就是 layout.tsx 和 template.tsx 的介紹。了解基本路由架構,以及 layout 和 template 怎麼使用後,接下來可能會產生另個疑問:假如我的商品頁網址是 /product/[商品id],那我有 100 個商品,我就要在 /product 中建 100 個資料夾嗎?

當然不可能,這時候就可以使用動態路由。這部分就留到明天介紹囉!

謝謝大家耐心的閱讀,我們明天見!


上一篇
Day 17 - Next.js 13 App Router 基本路由設定
下一篇
Day 19 - Next.js 13 App Router 動態路由 Dynamic Routes & getStaticParams()
系列文
深入淺出,完整認識 Next.js 13 !30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言